#pragma once

#include "common.hpp"
#include "log.hpp"
#include "win.hpp"

#include <openssl/bio.h>
#include <openssl/conf.h>
#include <openssl/err.h>
#include <openssl/ssl.h>

#include <utility>
/**
 * @brief a simple double thread socket client
 *
 * use std::function as socket operation callback
 */
class client {
public:
    using recv_hook_t = std::function<bool(std::vector<char>)>;
    using close_hook_t = std::function<void()>;

    client() {
        _recv_hook = [](std::vector<char> buf) {
            linfo("received " << buf.size() << " bytes from server: \n"
                              << std::string(buf.data(), std::min(buf.size(), static_cast<size_t>(100))));
            return true;
        };
        _close_hook = []() { linfo("connection closed."); };
    }

    client(const client&) = delete;
    client& operator=(const client&) = delete;
    client(client&&) = delete;
    client& operator=(client&&) = delete;

    ~client() {
        close();
        if (_recv_thread.joinable()) {
            _recv_thread.join();
        }
    }

    [[nodiscard]] bool alive() const {
        return _close_flag.load();
    }

    void initialize();

    [[nodiscard]] bool send(std::vector<char> buf);

    [[nodiscard]] bool connect(const std::string& address, u_short port, recv_hook_t recv_hook = nullptr);

    void close();

    void register_recv_hook(recv_hook_t recv_hook) {
        _recv_hook = std::move(recv_hook);
    }

    void register_close_hook(close_hook_t close_hook) {
        _close_hook = std::move(close_hook);
    }

private:
    void _initialize_socket();

    recv_hook_t _recv_hook{nullptr};
    close_hook_t _close_hook{nullptr};
    win::socket _socket{INVALID_SOCKET};
    std::thread _recv_thread;
    bool _initialized{false};
    bool _connected{false};

    std::mutex _m;

    std::atomic<bool> _close_flag{false};
};

class secure_client {
public:
    using recv_hook_t = std::function<bool(std::vector<char>)>;
    using close_hook_t = std::function<void()>;

    secure_client(const secure_client& other) = delete;
    secure_client(secure_client&& other) noexcept = delete;
    secure_client& operator=(const secure_client& other) = delete;
    secure_client& operator=(secure_client&& other) noexcept = delete;

    secure_client() {
        win::initialize_socket_library();
        initialize();
        _recv_hook = [](std::vector<char> buf) {
            linfo("received " << buf.size() << " bytes from server: \n"
                              << std::string(buf.data(), std::min(buf.size(), static_cast<size_t>(100))));
            return true;
        };
        _close_hook = []() { linfo("connection closed."); };
    }

    ~secure_client() {
        close();
        if (_ssl_ctx) {
            SSL_CTX_free(_ssl_ctx);
            _ssl_ctx = nullptr;
        }
    }

    [[nodiscard]] bool alive() const {
        return _close_flag.load();
    }

    void initialize() {
        _ssl_ctx = SSL_CTX_new(TLS_client_method());
        assert(SSL_CTX_set_default_verify_paths(_ssl_ctx), "setting up truct store failed");
    }

    [[nodiscard]] bool send(std::vector<char> buf) {
        if (!_connected) {
            lwarn("server not connected.");
            return false;
        }
        std::scoped_lock<std::mutex> lock(_m);
        if (BIO_write(_bio, buf.data(), int(buf.size())) < 0) {
            lwarn("server disconnected.");
            close();
            return false;
        }
        return true;
    }

    [[nodiscard]] bool connect(const std::string& address, u_short port, recv_hook_t recv_hook = nullptr) {
        _bio = BIO_new_connect((address + ":" + std::to_string(port)).data());
        if (!_bio) {
            lerror("BIO_new_connect failed");
            return false;
        }
        if (!BIO_do_connect(_bio)) {
            lerror("BIO_do_connect failed");
            return false;
        }
        _bio = BIO_push(_bio, BIO_new_ssl(_ssl_ctx, 1));
        auto ssl = _get_ssl(_bio);
        if (!ssl) {
            lerror("BIO_get_ssl failed");
            return false;
        }
        SSL_set_tlsext_host_name(ssl, address.data());
        SSL_set1_host(ssl, address.data());
        if (BIO_do_handshake(_bio) <= 0) {
            lerror("BIO_do_handshake failed");
            return false;
        }

        _connected = true;

        if (recv_hook) {
            _recv_hook = std::move(recv_hook);
        }

        _close_flag.store(true);

        _recv_thread = std::thread([this]() {
            while (_close_flag.load()) {
                std::vector<char> buf(4096, 0);
                auto ret = BIO_read(_bio, buf.data(), int(buf.size()));
                if (ret < 0) {
                    if (_connected) {
                        lerror("BIO_read failed, server shutdown");
                        close();
                    }
                    break;
                }
                if (ret == 0) {
                    if (BIO_should_retry(_bio)) {
                        continue;
                    }
                    lwarn("empty BIO_read, server closed");
                    break;
                }
                buf.resize(ret);
                if (!_recv_hook(std::move(buf))) {
                    break;
                }
            }
            linfo("receive thread closed");
        });

        return true;
    }

    void close() {
        if (!_connected) {
            return;
        }
        if (_bio) {
            BIO_free_all(_bio);
            _bio = nullptr;
        }

        _connected = false;
        _close_flag.store(false);
        if (_recv_thread.get_id() != std::this_thread::get_id()) {
            _recv_thread.join();
        }
        if (_close_hook) {
            _close_hook();
        }
    }

    void register_recv_hook(recv_hook_t recv_hook) {
        assert(recv_hook, "recv_hook should not be null");
        _recv_hook = std::move(recv_hook);
    }

    void register_close_hook(close_hook_t close_hook) {
        _close_hook = std::move(close_hook);
    }

private:
    static SSL* _get_ssl(BIO* bio) {
        SSL* ssl = nullptr;
        BIO_get_ssl(bio, &ssl);
        return ssl;
    }

    static bool verify_the_certificate(SSL* ssl, const std::string&) {
        int err = SSL_get_verify_result(ssl);
        if (err != X509_V_OK) {
            const char* message = X509_verify_cert_error_string(err);
            lerror("Certificate verification error: " << message << " " << err);
            return false;
        }
        X509* cert = SSL_get_peer_certificate(ssl);
        if (cert == nullptr) {
            lerror("No certificate was presented by the server\n");
            return false;
        }
        return true;
    }

    std::atomic_bool _close_flag{false};
    std::thread _recv_thread;

    std::mutex _m;

    bool _connected{false};

    SSL_CTX* _ssl_ctx{nullptr};
    BIO* _bio{nullptr};

    recv_hook_t _recv_hook{nullptr};
    close_hook_t _close_hook{nullptr};
};